﻿# import standard python modules
import pygame, os, time, thread
from pygame.locals import *
from Tkinter import *
from tkFileDialog   import askopenfilename 

# import modules such as task, queue, cpu from modules folder
path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../modules'))
if not path in sys.path:
  sys.path.insert(1, path)
del path

from constants import *
from task import Task
from queue import Queue
from vlist import List
from cpu import CPU
from simulator import *

#####################################################
#                                                   #
#  LoadSimulation class:                            # 
#                                                   #
#  This class loads a previously                    #
#  saved simulation and runs it.                    #
#                                                   # 
#  Author: Felix Rohrer                             #     
#  Course: CS MET 575                               #
#  Year:   2011                                     #
#                                                   #
#####################################################

class LoadSimulation():
  def __init__(self, mainMenu, tasks=[]):

    # main menu object
    self.mainMenu = mainMenu

    self.frame = mainMenu.ctrlRegion

    # variable text for run/pause button
    self.runPauseText = StringVar()
    self.runPauseText.set("Pause")

    # label to display information
    self.infoMsg = StringVar()
    self.infoMsgLabel = Label(self.frame, textvariable=self.infoMsg, padx=10, justify=LEFT, anchor=W, relief = GROOVE, width=32)
    
    # recording related variables
    self.rawContent = ''
    self.flow = []
    # first token in flow list contains amount of tasks
    # and does not need to be checked while simulation is running
    self.curMajorStep = 0
    self.curMinorStep = 0
    self.simulating = False

    # draw area color
    self.drawAreaColor = DRAWAREA_BG_COLOR
    self.drawAreaColorHex = '#%02X%02X%02X' % self.drawAreaColor

    # all objects
    self.tasks = []
    self.origTaskPos = {}
    self.ds = {}
    self.numTasks = 0

  # destroy the 'load simulation' control panel with all 
  # its dependent windows
  def destroy(self):

    # close the simulation window if opened
    self.closeSimulation()

    # wait until simulation stopped
    while self.mainMenu.simulationRunning:
      pass

    # do not show any information
    self.displayInfo(None)

    self.frame = None
    self.mainMenu = None

  # display information string in main window
  def displayInfo(self,infoMsg, pos=(0,20,3),color=ERR_BG_COLOR):
    if infoMsg is None:
      self.infoMsg.set('')
      self.infoMsgLabel.config(bg=MAINMENU_BG_COLOR,relief=FLAT)
    else:
      self.infoMsg.set(infoMsg)
      self.infoMsgLabel.config(bg=color,relief=GROOVE)
    self.infoMsgLabel.grid(column=pos[0],row=pos[1],columnspan=pos[2],sticky=W)

  def loadFile(self):
    fileName = ''
    try:
      fileName = askopenfilename(initialdir=SIMFOLDER)
    except:
      self.displayInfo("Error: unable to load simulation. ")
    if fileName is not '' and fileName is not ():
      fh = open(fileName,'r')
      self.rawContent = fh.read()
      fh.close()
      self.startButton.config(state=NORMAL)
      self.displayInfo("Loading successful: Click 'Start Simulation' \nto begin. ",color=INFO_BG_COLOR)
    else:
      self.displayInfo("No simulation loaded. ",color=INFO_BG_COLOR)

  # run or pause current simulation
  # if simulation currently running, pause simulation
  # if simulation currently pausing, resume simulation
  def runPauseSimulation(self):
    if self.simulating:
      self.runPauseText.set("Run")
      self.simulating = False
      self.displayInfo("Simulation paused. ",color=INFO_BG_COLOR)
    else:
      self.runPauseText.set("Pause")
      self.simulating = True
      self.displayInfo("Simulation running. ",color=INFO_BG_COLOR)
    
  # restarts simulation
  def restartSimulation(self):
    self.simulating = False

    # make sure that no task is assigned to currentTask
    self.currentTask = None

    # reset each tasks position and current execution time
    for t in self.tasks:
      t.resetExecTime()
      t.setPos(self.origTaskPos[t.name])

    # reset all data structures
    for v in self.ds.itervalues():
      v.makeEmpty()

    # reset major and minor step of simulation
    self.curMajorStep = 0
    self.curMinorStep = 0

    # make sure tasks that belong to a certain data structure
    # are correctly added to that data structure before
    # starting the simulation
    self.initializeDS()

    # set run-pause button to default value
    self.runPauseText.set("Pause")

    self.simulating = True
    self.displayInfo("Simulation running. ",color=INFO_BG_COLOR)

  # check if any tasks have been added to any data structure
  # before the recording got started
  def initializeDS(self):
    for v in self.ds.itervalues():
      belongsToQueue = []
      dst = v.DSType
      for t in self.tasks:
        currRect = Rect(t.pos[0],t.pos[1],t.size[0],t.size[1])
        listPos = currRect.collidelist(v.rects)

        if listPos >= 0:
	  if dst == 'QUEUE':
            belongsToQueue.append(t)
	  elif dst == 'LIST':
	    v.insert(t,listPos)
      # tasks have to be added to queue in the right order
      if dst == 'QUEUE':
        # sort elements in queue according to their position
        # adding task from left to right, relative to their position
        belongsToQueue = sorted(belongsToQueue, key=lambda t: t.pos)
        for t in belongsToQueue:
          v.push(t)

  def initializeSimulation(self,tasks,ds):
    # initially split content into
    # initialization part and execution part
    self.init,sep,flow = self.rawContent.partition('END_INITIAL')

    self.initParts = self.init.split(';')

    try:
      # add tasks, queues, lists and CPUs to simulation
      for p in self.initParts:
	if p is not '':
	  # example string item: l11-3-[327, 203]
	  item = p.split('-')
	  partPos = map(int, item[2][1:-1].split(','))
	  if p[0] == 't':
	    t = Task(item[0][2:])
	    t.setPos(partPos)
	    tasks.append(t)
	    self.origTaskPos[item[0][2:]] = list(t.getPos())
	  elif p[0] == 'q':
	    ds[p[0:2]] = Queue(int(item[1]),partPos,item[0][2:])
	  elif p[0] == 'l':
	    ds[p[0:2]] = List(int(item[1]),partPos,item[0][2:])
	  elif p[0] == 'c':
	    ds[p[0:2]] = CPU(item[0][2:],partPos)
    except:
      return False

    # create list from flow string
    self.flow = flow.split(';')

    self.curMajorStep = 0
    self.curMinorStep = 0
    self.currentTask = None
    self.simulating = True
    self.displayInfo("Simulation running. ",color=INFO_BG_COLOR)
    return True

  # every simulation needs to provide a controls function
  # the controls function will be called from the main menu, 
  # once this specific simulation is requested
  def controls(self):
    self.frame.grid()

    self.frame.grid_columnconfigure(0,minsize=150)

    # open file dialog
    self.openLabel = Label(self.frame, text="Load simulation:", height=2,font=TITLE_FONT)
    self.openButton = Button(self.frame, text="Open file", command=self.loadFile,width=15)

    # start saved simulation
    self.startLabel = Label(self.frame, text="Start simulation:", height=2,font=TITLE_FONT)
    self.startButton = Button(self.frame, text="Start simulation", command=self.startSimulation, state=DISABLED,width=15)
    self.closeButton = Button(self.frame, text="Close simulation", command=self.closeSimulation, width=15,state=DISABLED)

    # control simulation
    self.controlLabel = Label(self.frame, text="Control simulation:", height=2,font=TITLE_FONT)
    self.runButton = Button(self.frame, textvariable=self.runPauseText, command=self.runPauseSimulation, state=DISABLED,width=5)
    self.restartButton = Button(self.frame, text="Restart", command=self.restartSimulation, state=DISABLED,width=5)

    # information label
    self.infoLabel = Label(self.frame, text="Information:", height=2,font=TITLE_FONT)

    # open
    self.openLabel.grid(column=0,row=0,sticky=W)
    self.openButton.grid(column=0,row=1,sticky=W)

    # start
    self.startLabel.grid(column=0,row=2,sticky=W)
    self.startButton.grid(column=0,row=3,sticky=W)
    self.closeButton.grid(column=0,row=4,sticky=W)

    # control
    self.controlLabel.grid(column=0,row=5,sticky=W)
    self.runButton.grid(column=0,row=6,sticky=W)
    self.restartButton.grid(column=0,row=6,sticky=W,padx=80)
  
    # information
    self.infoLabel.grid(columnspan=2,column=0,row=7,sticky=W)

    self.displayInfo("Click 'Open File' to load \na simulation. ",color=INFO_BG_COLOR)

  # close simulation window
  def closeSimulation(self):
    self.going = False

  # every simulation needs this function in order to create
  # a new thread that will start the actual simulation window
  # A new thread is required to keep the main menu responsive
  def startSimulation(self):

    tasks = []
    ds = {}

    # simulating is only False if a previous simulation was run
    if not self.simulating:
      # initialize simulation
      if not self.initializeSimulation(tasks,ds):
        self.displayInfo("Error: Simulation malformed, cannot run. \n")
        return

    # only start new simulation if no other simulation running
    if not self.mainMenu.simulationRunning:

      # disable start button (only one simulation at a time allowed)
      self.startButton.config(state=DISABLED)
      self.openButton.config(state=DISABLED)
     
      # enable control buttons
      self.runButton.config(state=NORMAL)
      self.restartButton.config(state=NORMAL)

      # start new thread to run simulation
      # -> keeps main menu responsive
      # self.plot(tasks)
      thread.start_new_thread(self.plot,(tasks,ds))
      self.mainMenu.simulationRunning = True
    else:
      self.displayInfo("Error, another simulation \nalready running. ")

  def getNextStep(self):

    def incMajorStep():
      self.curMajorStep += 1
      self.curMinorStep = 0

    if self.curMajorStep < len(self.flow):
      major = self.flow[self.curMajorStep]

      # make sure major string is at least
      # 2 characters long
      if len(major) < 2:
        return False
      stepType = major[0]
      stepValue = major[1:]
      if stepType is 'c':
        incMajorStep()
        return ('MOVE',tuple(map(int, stepValue[1:-1].split(','))))
      elif stepType is 'a':
        for t in self.tasks:
          if t.name == stepValue:
            incMajorStep()
            return ('ADD', t)
      elif stepType is 'p':
        if self.curMinorStep < int(stepValue):
          self.curMinorStep += 1
        else:
          incMajorStep()
        return ('NOP',)
      elif stepType is 'd':
        incMajorStep()
        return ('DROP',)
    return False

  # returns False if task could not be removed
  def removeFromCurrentQueue(self, task):
    if task.getDSType() == 'QUEUE':
      if task.currentDS.peakNext() == task:
        task.currentDS.pop()
      else:
        # in a queue, only first element can be removed
        return False
    elif task.getDSType() == 'LIST':
      task.currentDS.remove(task)
    return True

  # check collision with any of the data structures
  def collisionWithDS(self, draggedTask):
    currRect = Rect(draggedTask.pos[0],draggedTask.pos[1],draggedTask.size[0],draggedTask.size[1])
    found = False
    for v in self.ds.itervalues():
      listPos = currRect.collidelist(v.rects)
      # currently dragged task is colliding with data structure
      if listPos >= 0:
        # get data structure type (queue or list)
        dst = v.DSType

        # if currently dragged task can be removed from its
        # old data structure (or does not belong to a data structure)
        # add the currently dragged task to this new data structure
        if self.removeFromCurrentQueue(draggedTask):
	  if dst == 'QUEUE':
	    v.push(draggedTask)
	    found = True
	  elif dst == 'LIST':
	    v.insert(draggedTask,listPos)
	    found = True
    # if currently dragged task did not collide with 
    # any data structure, the task is on a non-data structure
    # field and its current data structure should be removed
    if not found:
      self.removeFromCurrentQueue(draggedTask)

  # plot the graph and all additional buttons and input fields 
  def plot(self,tasks=[],ds={}):

    # create clock to limit FPS
    clock = pygame.time.Clock()

    os.environ['SDL_VIDEO_WINDOW_POS'] = DRAWAREA_INIT_POS

    self.tasks = tasks
    self.ds = ds

    # initialize simulation window
    pygame.init()
    pygame.display.init()

    self.going = True
    self.screenDim = {'width': DRAWAREA_WIDTH, 'height': DRAWAREA_HEIGHT}

    self.screen = pygame.display.set_mode ((self.screenDim['width'], self.screenDim['height']))
    pygame.display.set_caption("Run saved simulation")

    background = pygame.Surface(self.screen.get_size())
    background = background.convert()

    # drawing area
    drawSize = self.screenDim['width'],self.screenDim['height']
    drawArea = pygame.Surface(drawSize)
    drawArea.fill(self.drawAreaColor)

    # define font
    textFont = pygame.font.SysFont(SYS_FONT,SYS_FONT_SIZE)
    textFont.set_bold(True)

    self.currentTask = None

    # enable and disable buttons
    self.closeButton.config(state=NORMAL)
    self.startButton.config(state=DISABLED)

    # sort data structures from dictionary
    sortedDS = self.ds.keys()
    sortedDS.sort()
    
    # check if any tasks have been added to any data structure
    # previous to recording
    self.initializeDS()

    while self.going:

      # limit frame rate
      time_passed = clock.tick(FPS)

      event = pygame.event.poll()
      if event.type == QUIT:
        self.going = False
      if event.type == KEYDOWN:
        if event.key == K_ESCAPE:
          self.going = False

      # during simulation, no mouse clicks allowed
      if event.type == MOUSEBUTTONDOWN:
        self.displayInfo("Error: No user interaction allowed \nduring simulation. ")
        
      # clear error message that occured when mouse click detected
      if event.type == MOUSEBUTTONUP:
        self.displayInfo(None)

      # draw graph
      self.screen.blit(background, (0, 0))

      # draw drawing area
      self.screen.blit(drawArea, (0,0))

      # update the drawing area
      drawArea.fill(self.drawAreaColor)
      pygame.draw.rect(drawArea, (0,0,0), (0,0,drawSize[0],drawSize[1]),1)

      if self.simulating:
        # get next step
        nextStep = self.getNextStep()

        if nextStep is False:
          self.simulating = False
          continue

        if nextStep[0] is 'NOP':
          # do nothing in this turn
          pass
        elif nextStep[0] is 'MOVE':
          if self.currentTask is not None:
            self.currentTask.setPos(nextStep[1],True)
          else:
            print 'Error: No task in currentTask!'
        elif nextStep[0] is 'ADD':
          if self.currentTask is None:
            self.currentTask = nextStep[1]
          else:
            print 'Error: There is already a task in currentTask!'
        elif nextStep[0] is 'DROP':
          if self.currentTask is not None:

            # check for collision with any data structure
            self.collisionWithDS(self.currentTask)

            self.currentTask.dropped()
            self.currentTask = None
          else:
            print 'Error: No task in currentTask!'

      # draw all data structures
      for k in sortedDS:
        self.ds[k].display(drawArea)

      # draw all tasks
      for t in self.tasks:
        t.display(drawArea,updateExecTime=self.simulating)

      pygame.display.flip()

    # indicate to main menu, that simulation is not running
    self.displayInfo("Stopping simulation...")
    self.frame.update_idletasks()

    # indicate to this class that no simulation is currently running
    self.simulating = False

    # restore task counter to 0
    Task.TASK_COUNT = 0

    # uninitialize queue object
    del self.tasks[:]
    self.ds.clear()
    self.numTasks = 0

    # enable start and open button
    self.closeButton.config(state=DISABLED)
    self.startButton.config(state=NORMAL)
    self.openButton.config(state=NORMAL)
   
    # disable control buttons
    self.runButton.config(state=DISABLED)
    self.restartButton.config(state=DISABLED)
    self.runPauseText.set("Pause")

    pygame.quit ()
    self.displayInfo("Open another file or press \n'Start Simulation' to begin. ",color=INFO_BG_COLOR)
    self.mainMenu.simulationRunning = False
